package core.impl;

import gui.Forme;
import gui.TableauBlancUI;

import java.lang.reflect.InvocationTargetException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.Semaphore;

import javax.swing.SwingUtilities;

import Utils.CLogger;

import launcher.Lanceur;
import core.api.ClientAPI;
import core.api.FormeAPI;
import core.api.ResoAPI;
import core.protocol.ProtocolAD;
import core.protocol.ProtocolAD.Type;
import core.protocol.impl.Bully;
import core.protocol.impl.ChangRoberts;
import core.protocol.impl.DolevKlaweRodeh;

public class Client extends UnicastRemoteObject implements ClientAPI{

	/**
	 * 
	 */
	private static final long serialVersionUID = -2800648127758756971L;

	private final ResoAPI resoAPI;
	private ProtocolAD protocol;
	private Type type;
	
	private CLogger cLogger;

	private int nextForm = 0;
	private Map<Integer,FormeAPI> forms = Collections.synchronizedMap(new TreeMap<Integer,FormeAPI>());

	private int idClient = -1;
	private int masterId = -1;
	private final ArrayList<Integer> clientID = new ArrayList<Integer>();
	private boolean isMaster = false;
	private boolean isConnected = false;
	private boolean isWaiting = false;

	private Semaphore semaphore = new Semaphore(0);

	private TableauBlancUI tableau;


	public Client(Type type) throws RemoteException,NotBoundException
	{
		super();
		this.type = type;
		
		Registry registry = LocateRegistry.getRegistry(Lanceur.DEFAULT_PORT);
		resoAPI = (ResoAPI) registry.lookup(Lanceur.NAMERESO);

		registry.rebind("Client"+getId(), this);

		final Client itsme = this;
		Runnable exR = new Runnable() {
			public void run() {
				tableau = new TableauBlancUI(itsme);
				tableau.setTitle("ID : "+itsme.idClient+" - MASTER : "+itsme.masterId);
			}
		};
		SwingUtilities.invokeLater(exR);
	}

	public boolean connect(){
		if(!isConnected)
			try {
				resoAPI.registerClient(this);
				cLogger = new CLogger("Reso");
				cLogger.getcLogger().info("Client "+this.getId()+" : connect");
				isConnected = true;
				
				createProtocol();
				
				for(int i : clientID){
					protocol.addNeighbour(i);
				}
				
				if(!clientID.isEmpty()){
					resoAPI.getMaster(idClient, clientID.get(0));
					resoAPI.getForms(idClient, clientID.get(0));
				}
				
				return true;
			} catch (RemoteException e) {
				e.printStackTrace();
			}
		return false;
	}

	private void createProtocol(){
		switch (type) {
		case Bully:
			protocol = new Bully(this);
			break;
		case ChangRobert:
			protocol = new ChangRoberts(this);
			break;
		case DolevKlaweRodeh:
			protocol = new DolevKlaweRodeh(this);
			break;
		default:
			break;
		}
	}
	
	public boolean deconnect(){
		if(isConnected)
		{
			try {
				resoAPI.unregisterClient(this);
				isConnected = false;
				protocol = null;
				clientID.clear();
				setMaster(-1);
				forms.clear();
				tableau.refresh();
				cLogger.getcLogger().info("Client "+this.getId()+" : deconnect");
				return true;
			} catch (RemoteException e) {
				e.printStackTrace();
			}
		}
		return false;
	}

	@Override
	public void acquire(int source, int dest) throws RemoteException {
		if(!isMaster)
		{
			System.out.println("Erreur : La semaphore ne peut etre manipuler que par le maitre");
		}
		
		try {
			cLogger.getcLogger().info(idClient + " wait lock");
			semaphore.acquire();
			cLogger.getcLogger().info(idClient + " get lock");
			resoAPI.confirmed(idClient, source, nextForm++);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	@Override
	public void confirmed(int source, int dest, int idForm) throws RemoteException {
		cLogger.getcLogger().info(idClient + " release lock");
		semaphore.release();
		nextForm = Math.max(nextForm, idForm);
	}

	@Override
	public void release(int source, int dest) throws RemoteException {
		if(!isMaster)
		{
			
			cLogger.getcLogger().warning("Erreur : La semaphore ne peut etre manipuler que par le maitre");
		}
		if(semaphore.availablePermits() > 0)
		{
			cLogger.getcLogger().warning("NE DOIT PAS ARRIVER");
		}else{
			cLogger.getcLogger().info(idClient + " release lock");
			semaphore.release();
		}
	}

	@Override
	public synchronized void sendToTrace(int source, int dest, int idForm, FormeAPI forme) throws RemoteException
	{
		if(!isConnected)
			return;
		
		forms.put(idForm,forme);
		nextForm = Math.max(nextForm, idForm + 1);
		tableau.refresh();
		cLogger.getcLogger().info("sendToTrace : " +source+"|"+dest+"|"+idForm+"|"+forme.toString());
	}

	@Override
	public int getId() throws RemoteException{
		return idClient;
	}

	@Override
	public void setId(int id) throws RemoteException{
		this.idClient = id;
	}

	public void addForme(final Forme forme){
		if(!isConnected)
			return;
		
		try {
			if(!isMaster)
				resoAPI.acquire(idClient, masterId);

			try {
				isWaiting = true;
				cLogger.getcLogger().info(idClient + " wait lock");
				semaphore.acquire();
				cLogger.getcLogger().info(idClient + " get lock");
				isWaiting = false;
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			forms.put(nextForm, forme);
			for(int client : clientID)
				resoAPI.sendToTrace(idClient, client, nextForm, forme);
			
			nextForm++;
			
			if(!isMaster)
				resoAPI.release(idClient, masterId);
			else{
				cLogger.getcLogger().info(idClient + " release lock");
				semaphore.release();
			}
			
		} catch (RemoteException e) {
			e.printStackTrace();
		}
	}
	
	public synchronized Collection<FormeAPI> getFormes(){
		return forms.values();
	}

	private void setMaster(int iID){
		masterId = iID;
		if(tableau != null)
			tableau.setTitle("ID : "+this.idClient+" - MASTER : "+this.masterId);

		boolean isMasterNow = (masterId == idClient);
		
		if(isMasterNow && !isMaster){
			cLogger.getcLogger().info(idClient + " release lock");
			semaphore.release();
		}else if(!isMasterNow && isMaster){
			try {
				cLogger.getcLogger().info(idClient + " wait lock");
				semaphore.acquire();
				cLogger.getcLogger().info(idClient + " get lock");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}else if(isWaiting && masterId >= 0){
			try {
				resoAPI.acquire(idClient, masterId);
			} catch (RemoteException e) {
				e.printStackTrace();
			}
		}
		
		isMaster = isMasterNow;
		
		cLogger.getcLogger().info("setMaster : " +iID);
	}

	@Override
	public void addClient(int iId) throws RemoteException{
		clientID.add(iId);
		if(protocol != null)
			protocol.addNeighbour(iId);
	}

	public TableauBlancUI getTableau() {
		return tableau;
	}
	
	@Override
	public void callProtocol(int source, int dest, String method, Object ... objects) throws RemoteException {

		if(!isConnected)
			return;
		try 
		{
			String tmp ="callProtocol : "+source+"|"+dest+"|"+method.toString();
			for(int i = 0; i < objects.length; i++)
			{
				tmp += "|"+objects[i].toString();
			}
			cLogger.getcLogger().info(tmp);
			
			resoAPI.callProtocol(source , dest, method, objects);
		} catch (RemoteException e) {
			e.printStackTrace();
		}
	}

	public ProtocolAD getProtocol() {
		return protocol;
	}

	@Override
	public void invokeProtocol(int source, int dest, String method, Object... objects) throws RemoteException {

		if(!isConnected)
			return;

		try {			
			Class<?> []classs = new Class[objects.length];
			for(int i = 0; i < objects.length; i ++)
				classs[i] = objects[i].getClass();
			protocol.getClass().getMethod(method, classs).invoke(protocol, objects);
			String tmp ="callProtocol : "+source+"|"+dest+"|"+method.toString();
			for(int i = 0; i < objects.length; i++)
			{
				tmp += "|"+objects[i].toString();
			}
			cLogger.getcLogger().info(tmp);
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void timeout(int dest) throws RemoteException {
		removeId(dest);
		if(dest == masterId){
			getProtocol().startElection();
			cLogger.getcLogger().info(idClient + "start election" +dest);
		}
		cLogger.getcLogger().info("timeout : " +dest);
	}
	
	@Override
	public void timeoutProtocol(int dest) throws RemoteException {
		removeId(dest);
		if( protocol != null){
			protocol.timeoutProtocol(dest);
		}
		cLogger.getcLogger().info("timeout (protocol): " +dest);
	}

	private void removeId(Integer id) {
		clientID.remove(id);
		protocol.removeNeighbour(id);
		cLogger.getcLogger().info("removeId :" +id);
	}

	public int getMasterId() {
		return masterId;
	}

	@Override
	public void getMaster(int source, int dest) throws RemoteException {
		cLogger.getcLogger().info("getMaster :" +source+"|"+dest);
		resoAPI.setMaster(idClient, source, masterId);
	}

	@Override
	public void getForms(int source, int dest) throws RemoteException {
		cLogger.getcLogger().info("getForms :" +source+"|"+dest);
		for(Entry<Integer, FormeAPI> entry : forms.entrySet())
			resoAPI.sendToTrace(idClient, source, entry.getKey(), entry.getValue());
	}

	@Override
	public void setMaster(int source, int dest, int master) throws RemoteException {
		setMaster(master);
		cLogger.getcLogger().info("setMaster :" +master);
	}

	public CLogger getcLogger() {
		return cLogger;
	}

	public void setcLogger(CLogger cLogger) {
		this.cLogger = cLogger;
	}

}
